昨天,我們替這支 Query 提出了一個 Index Design:
SELECT *
FROM orders
WHERE user_id = 123
AND status = 'completed'
ORDER BY created_at DESC;
CREATE INDEX idx_orders_user_status
ON orders(user_id, status);
但 Index 加上去之後,我們要怎麼知道它是不是真的有幫上忙呢?
今天就直接打開 EXPLAIN ANALYZE 看。
先從它到底在做什麼開始。
EXPLAIN ANALYZE是 PostgreSQL 用來顯示 Query Execution Plan(查詢執行計畫),並實際執行這支 Query、補上真實執行資料的工具。
例如昨天的 Query,只要在前面加上:
EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE user_id = 123
AND status = 'completed'
ORDER BY created_at DESC;
原本我們只會拿到查詢結果。
加上 EXPLAIN ANALYZE 後,看到的則會變成:
Query 到底走哪一條路?
用了哪個 Index?
Planner 原本估計要處理多少資料?
實際又處理了多少?
實際花了多少時間?
也就是把 Database 執行這支 Query 的過程攤開來看。EXPLAIN 可以看到 Planner 產生的 Execution Plan;加上 ANALYZE 後,Query 會真的被執行,並補上實際時間與 Row 數。
這裡也要先分清楚兩個很像的指令:
EXPLAIN
→ 看 Database「打算怎麼跑」
EXPLAIN ANALYZE
→ 真的跑一次,再把實際結果一起顯示
所以如果只是:
EXPLAIN
SELECT * FROM orders;
看到的是 Planner 的估算。
但:
EXPLAIN ANALYZE
SELECT * FROM orders;
除了原本的估算,還會多出真正執行後的 actual time、actual rows 等資訊。
這裡有一個很重要的差別:
EXPLAIN ANALYZE 真的會執行 Query。
如果今天只是:
SELECT ...
通常就是把查詢跑一次。
但如果寫的是:
EXPLAIN ANALYZE
DELETE FROM orders
WHERE ...;
那筆 DELETE 真的會發生。
INSERT、UPDATE 也一樣。
所以在會修改資料的 Query 上使用 EXPLAIN ANALYZE 時,不能把它當成單純的預覽工具。若只想觀察執行結果、不保留修改,可以放在 Transaction 裡執行後再 ROLLBACK。
知道 EXPLAIN ANALYZE 能看到執行方式後,下一個要先認識的是 Query Planner(查詢規劃器)。
假設我寫:
SELECT *
FROM orders
WHERE user_id = 123;
對我們來說,SQL 只寫了一句:
找出
user_id = 123的資料。
但 Database 還要決定:
要怎麼找?
它可能:
直接掃 Table
也可能:
先走 Index
再去拿需要的 Row
甚至還有其他方式。
PostgreSQL 的 Planner 會根據 Query、資料統計與成本估算,選一份它認為適合的 Execution Plan(執行計畫)。
所以可以先把整個關係想成:
SQL
↓
Query Planner
↓
選擇 Execution Plan
↓
Database 實際執行
EXPLAIN ANALYZE 做的,就是讓我們看到這條路最後怎麼走。
打開一份 Execution Plan,最先可以注意的是:
Database 選了哪一種 Scan?
PostgreSQL 裡常會看到:
Seq Scan
Index Scan
Bitmap Index Scan + Bitmap Heap Scan
假設昨天那張 orders 有 100 萬筆資料,其中 90 萬筆都是:
status = completed
現在查:
SELECT *
FROM orders
WHERE status = 'completed';
雖然 status 有可能存在 Index,但 Query 本來就要拿出 90 萬筆。
這時 Planner 可能判斷:
都要讀這麼多資料了,直接掃 Table 比一直透過 Index 找還划算。
Execution Plan 就可能看到:
Seq Scan on orders
Seq Scan 是 Sequential Scan(循序掃描),可以先理解成:
直接依序掃過 Table,找出符合條件的資料。
看到 Seq Scan,不代表 Query 一定寫壞了。
像這個例子,status = 'completed' 篩完還剩下 90% 的資料,Planner 直接掃 Table 反而可能比較划算。
再換回:
SELECT *
FROM orders
WHERE user_id = 123;
假設 user_id 有很多不同值,而 123 最後只會找到幾十筆。
如果已經建立:
CREATE INDEX idx_orders_user_id
ON orders(user_id);
Execution Plan 就可能看到:
Index Scan using idx_orders_user_id on orders
這時就很好讀了:
Index Scan
→ 這次選擇走 Index
using idx_orders_user_id
→ 使用的是這份 Index
如果下面還看到:
Index Cond: (user_id = 123)
就是在告訴我們:
這個條件被拿來使用 Index 找資料。
所以昨天問的:
「我建立的 Index 到底有沒有真的派上用場?」
在這裡就可以開始找到答案。
那如果符合條件的資料:
呢?
這時有可能看到:
Bitmap Index Scan
↓
Bitmap Heap Scan
可以先用很白話的方式理解:
Bitmap Index Scan
→ 先透過 Index 整理出「哪些位置有我要的資料」
Bitmap Heap Scan
→ 再回 Table 把那些資料取出來
所以三種方式可以先這樣看:
Seq Scan
→ 直接掃 Table
Index Scan
→ 用 Index 找少量資料
Bitmap
→ 先整理一批位置,再回 Table 取資料
實際走哪一條,還是由 Planner 根據 Query 和資料狀況決定。
知道 Database 選了哪條路後,再往右看一點。
Execution Plan 很常出現:
cost=0.29..8.31
第一次看到很容易把它讀成:
0.29 ms 到 8.31 ms?
但 cost 不是實際執行時間。
它可以先理解成:
Planner 預估「走這條 Execution Plan 要花多少工作量」的相對成本。
讀資料、處理 Row、做運算,都可能被算進這份估算裡,Planner 再拿不同 Plan 的 cost 互相比較。
cost=0.29..8.31
↑ ↑
startup total
cost cost
startup cost 可以先理解成:
產出第一筆 Row 前的預估成本。
total cost 則是:
假設這個 Node 全部跑完的預估總成本。
所以 8.31 不是 8.31 ms。
cost 看得是 Planner 的估算;真正執行後量到的時間,會出現在 actual time。
使用 EXPLAIN ANALYZE 後,會多看到:
actual time=0.020..0.080
這裡的時間才是實際執行量到的時間,而且單位是毫秒。
可以先這樣讀:
actual time=0.020..0.080
↑ ↑
第一筆 完成這個 Node
所以:
cost
→ Planner 原本怎麼估
actual time
→ 實際跑完後量到多少
這兩個不能混在一起看。
除了時間,我覺得 EXPLAIN ANALYZE 很有意思的地方,是可以直接把:
Database 原本以為會有多少資料
和:
實際真的有多少資料
放在一起看。
例如:
cost=0.29..8.31 rows=10
這裡的:
rows=10
是 Planner 的 Estimated Rows(預估資料筆數)。
意思是:
我估計這個 Plan Node 大概會產生 10 筆資料。
使用 EXPLAIN ANALYZE 後,又可能看到:
actual time=0.020..0.080 rows=12
這個:
rows=12
才是實際執行後真的產生 12 筆。
如果變成:
estimated rows = 10
actual rows = 5000
落差就很明顯了。
Planner 原本只估計會有 10 筆,實際卻有 5000 筆,代表它對這次 Query 會產生多少資料的估計差很多。
這時就值得繼續往資料統計、資料分布等方向檢查,而不是只盯著「有沒有 Index」。
前面的角色都認識之後了,讓我們一起來複習一下今天講的內容吧!
先回到昨天那支 Query:
EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE user_id = 123
AND status = 'completed'
ORDER BY created_at DESC;
這裡是為了教學簡化過的結果:
Index Scan using idx_orders_user_status on orders
(cost=0.29..8.31 rows=10 width=120)
(actual time=0.020..0.080 rows=12 loops=1)
Index Cond: (
user_id = 123
AND status = 'completed'
)
Planning Time: 0.150 ms
Execution Time: 0.100 ms
第一次看到這一大串,其實不用每個數字都讀。
先抓今天學的幾個地方。
Index Scan
代表這次是透過 Index 查找。
而:
using idx_orders_user_status
告訴我們,它使用的正是昨天建立的:
idx_orders_user_status
所以至少可以確認:
這次 Execution Plan 真的有使用這份 Index。
cost=0.29..8.31
rows=10
代表 Planner 估計:
8.31
10 筆 Row記得,8.31 不是 8.31 ms。
actual time=0.020..0.080
rows=12
實際執行後:
12 筆這次:
estimated = 10
actual = 12
兩邊很接近。
最後:
Execution Time: 0.100 ms
才是這一次 Query Execution 的整體執行時間。
所以拿到一份基本的 EXPLAIN ANALYZE,可以先照這個順序看:
走哪種 Plan?
↓
有沒有走想看的 Index?
↓
Planner 原本估多少?
↓
實際 rows 差多少?
↓
actual time / Execution Time 是多少?
先能回答這五件事,就已經可以開始用它檢查自己的 Query。
Vibe Coding 很容易在功能做完、資料還不多的時候,看起來一切都很正常。
直到資料慢慢增加,某支 API 或某個頁面開始變慢,才發現:
「怎麼現在要等這麼久?」
這時最直覺的做法可能就是把 Query 丟給 AI:
「這支查詢很慢,幫我優化。」
AI 可以幫忙改 SQL、補 Index,甚至直接產出修改好的 Code。
但如果自己不知道怎麼查,真正缺掉的是中間這一段:
到底是哪支 Query 慢?
↓
Database 現在怎麼執行?
↓
慢在掃太多資料、沒走 Index,
還是 Planner 的估算和實際差很多?
↓
修改之後真的改善了嗎?
EXPLAIN ANALYZE 看原本的 Execution Plan,再針對真正的原因修改,最後重新執行一次比較前後結果。AI 很適合幫忙提出優化方案。
但要知道該改什麼,得先知道 Database 現在到底慢在哪裡。
Index 讓我看到 Database Design 本身就會影響資料查找的效率。
而 EXPLAIN ANALYZE 更讓我覺得很神奇的是:
原來在 Database 裡,就有方法可以直接看到 Query 怎麼執行、實際花多少時間,甚至比較原本的估算和真正跑出來的結果。
這樣 Index 加完之後,也不只是看 Code 能不能跑,或憑感覺覺得好像變快了。
還可以真的把執行結果打開來看:
它到底有沒有幫上忙。
一路寫到這裡,這趟從 0→1 的工程世界也真的快走到終點了。
下一篇,就是這 30 天的最後一篇。
最後,就一起來回頭看看我們這一路都學了些什麼吧~